-module(rtps).

%%% @doc
-behaviour(gen_statem).

%% -include_lib("kernel/include/logger.hrl").
-include("rtps.hrl").

%% API
-export([start_link/1,
	 add_domain/1, add_domain/2, del_domain/1,
	 add_participant/2, add_participant/3, del_participant/2,
	 add_writer/4, add_writer/5, del_writer/3, del_writer/4,
	 add_reader/4, add_reader/5, del_reader/3, del_reader/4,
	 whereis/1, whereis/2,
	 whereis_writer/3, whereis_writer/4,
	 whereis_reader/3, whereis_reader/4,
	 info/0, info/1, info/2, info/3,
	 guid_prefix/0,
	 convtime/1, convtime/2, convtime_ms/1
	]).

%% gen_statem callbacks
-export([init/1, callback_mode/0,
	 running/3,
	 handle_event/4, 
	 terminate/3, code_change/4]).

%%%===================================================================
%%% Types
%%%===================================================================
-export_type([domain_id/0, participant_id/0, guid_prefix/0, entity_type/0, entity_kind/0,
	      entity_key/0, entity_id/0, guid/0, vendor_id/0, sequence_number/0,
	      sequence_number_set/0, fragment_number/0, fragment_number_set/0, seconds/0,
	      fraction/0, time/0, locator_kind/0, locator_port/0, locator_address/0,
	      locator/0, locator_udpv4/0, topic_kind/0, reliability_kind/0, count/0,
	      major_protocol_version/0, minor_protocol_version/0, protocol_version/0,
	      key_hash/0, status_info/0, content_filter_property/0, content_filter_info/0,
	      property/0, parameter/0, original_writer_info/0, builtin_endpoint_set/0,
	      duration/0,
	      submessage/0, msg/0, available_builtin_endpoints/0,
	      history_kind/0, history_depth/0, history_durability/0,
	      %% Options
	      domain_opt/0]).

%% Domain_id and Participant_id are limitted, depending on the
%% availability of ports.
-type domain_id() :: non_neg_integer(). % 0..?MAX_DOMAINS - 1. 
%% 9.6.1.1 The participantId identifier is used to avoid port
%% conflicts among Participants on the same node.
-type participant_id() :: non_neg_integer(). % 0..?MAX_PARTICIPANTS - 1.

%% 9.3.1.1 Mapping of the GuidPrefix
-type guid_prefix() :: unknown | <<_:96>>.

%% 9.3.1.2 Mapping of the EntityId
-type entity_type() :: user | built_in | vendor.
-type entity_kindu() :: unknown
		     | writer_with_key | writer_no_key 
		     | reader_no_key | reader_with_key.
-type entity_kindbi() :: entity_kindu() | participant.
-type entity_kind() :: entity_kindu() | entity_kindbi() | 0..16#3f.
-type entity_key() :: <<_:24>>. % 3 octets

%% 9.3.1.3 Predefined EntityIds
-type entity_id() :: participant | 'SEDPbuiltinTopicWriter' | 'SEDPbuiltinTopicReader'
		   | 'SEDPbuiltinPublicationsWriter' | 'SEDPbuiltinPublicationsReader'
		   | 'SEDPbuiltinSubscriptionsWriter' | 'SEDPbuiltinSubscriptionsReader'
		   | 'SPDPbuiltinParticipantWriter' | 'SPDPbuiltinParticipantReader'
		   | 'BuiltinParticipantMessageWriter' | 'BuiltinParticipantMessageReader'
		   | unknown
		   | #entity_id{type :: user, kind :: entity_kindu()}
		   | #entity_id{type :: built_in, kind :: entity_kindbi()}
		   | #entity_id{type :: vendor, kind :: 0..16#3f}.

-type guid() :: unknown | #guid{guid_prefix :: guid_prefix(), entity_id :: entity_id()}.

%% VendorId
-type vendor_id() :: <<_:16>> | unknown.

%% SequenceNumber
-type sequence_number() :: unknown | 1..16#7fffffffffffffff.
-type sequence_number_set() :: #sequence_number_set{}.

%% FragmentNumber
-type fragment_number() :: 0..16#ffffffff.
-type fragment_number_set() :: #fragment_number_set{}.

%% 9.3.2 Mapping of the Types that Appear Within Submessages or
%% Built-in Topic Data Time. NB: Internally, the application uses
%% regular Erlang time with the nanosecond unit.
-type seconds() :: 0..16#7fffffff.
-type fraction() :: 0..16#ffffffff.
-type time() :: zero | invalid | infinity | #time{}.
%% 8.4.6 Type Definitions
-type duration() :: time().

%% Locator

%% For the addresses and port numbers, Erlang's inet types are used
%% which differ from RTPS's for ipv6 addresses.
-type locator_kind() :: invalid | reserved | udpv4 | udpv6 | 3..16#7fffffff.
-type locator_port() :: invalid | inet:port_number().
-type locator_address() :: invalid | inet:ip4_address() | inet:ip6_address() | <<_:128>>.
-type locator_udpv4() :: #locator_udpv4{}.

-type locator() :: invalid
		 | #locator{kind :: udpv4, port :: locator_port(), address :: inet:ip4_address()}
		 | #locator{kind :: udpv6, port :: locator_port(), address :: inet:ip6_address()}
		 | #locator{kind :: reserved, port :: locator_port(), address ::  <<_:128>>}
		 | #locator{kind :: 3..16#7fffffff, port :: locator_port(), address ::  <<_:128>>}.

%% TopicKind
-type topic_kind() :: no_key | with_key | 0 | 3..16#7fffffff.
%% ReliabilityKind. See table 9.4 p156. Two types are predefined. 
-type reliability_kind() :: best_effort | reliable | 0 | 2 | 4..16#7fffffff.

%% Count
-type count() :: 0..16#7fffffff.

%% Version
-type major_protocol_version() :: 0..16#ff.
-type minor_protocol_version() :: 0..16#ff.
-type protocol_version() :: #protocol_version{}.

%% KeyHash
-type key_hash() :: 0..16#ffffffffffffffffffffffffffffffff.
%% StatusInfo
-type status_info() :: 0..16#7fffffff.

%% ContentFilterProperty
-type content_filter_property() :: #content_filter_property{}.

%% ContentFilterInfo
-type content_filter_info() :: #content_filter_info{}.

%% Property
-type property() :: #property{}.

%% Parameter list
-type parameter() :: #parameter{id :: user_data, value :: binary()} |
		     #parameter{id :: topic_name, value :: string()} |
		     #parameter{id :: type_name, value :: string()} |
		     #parameter{id :: group_data, value :: binary()} |
		     #parameter{id :: topic_data, value :: binary()} |
		     #parameter{id :: durability, value :: binary()} |
		     #parameter{id :: durability_service, value :: binary()} |
		     #parameter{id :: deadline, value :: binary()} |
		     #parameter{id :: latency_budget, value :: binary()} |
		     #parameter{id :: liveliness, value :: binary()} |
		     #parameter{id :: reliability, value :: binary()} |
		     #parameter{id :: lifespan, value :: binary()} |
		     #parameter{id :: destination_order, value :: binary()} |
		     #parameter{id :: history, value :: binary()} |
		     #parameter{id :: resource_limits, value :: binary()} |
		     #parameter{id :: ownership, value :: binary()} |
		     #parameter{id :: ownership_strength, value :: binary()} |
		     #parameter{id :: presentation, value :: binary()} |
		     #parameter{id :: partition, value :: binary()} |
		     #parameter{id :: time_based_filter, value :: binary()} |
		     #parameter{id :: transport_priority, value :: binary()} |
		     #parameter{id :: protocol_version, value :: protocol_version()} |
		     #parameter{id :: vendor_id, value :: vendor_id()} |
		     #parameter{id :: unicast_locator, value :: locator()} |
		     #parameter{id :: multicast_locator, value :: locator()} |
		     #parameter{id :: multicast_ipaddress, value :: inet:ip4_address()} |
		     #parameter{id :: default_unicast_locator, value :: locator()} |
		     #parameter{id :: default_multicast_locator, value :: locator()} |
		     #parameter{id :: metatraffic_unicast_locator, value :: locator()} |
		     #parameter{id :: metatraffic_multicast_locator, value :: locator()} |
		     #parameter{id :: default_unicast_ipaddress, value :: inet:ip4_address()} |
		     #parameter{id :: default_unicast_port, value :: inet:port_number()} |
		     #parameter{id :: metatraffic_unicast_ipaddress, value :: inet:ip4_address()} |
		     #parameter{id :: metatraffic_unicast_port, value :: inet:port_number()} |
		     #parameter{id :: metatraffic_multicast_ipaddress, value :: inet:ip4_address()} |
		     #parameter{id :: metatraffic_multicast_port, value :: inet:port_number()} |
		     #parameter{id :: expects_inline_qos, value :: boolean()} |
		     #parameter{id :: participant_manual_liveliness_count, value :: count()} |
		     #parameter{id :: participant_builtin_endpoints, value :: non_neg_integer()} |
		     #parameter{id :: participant_lease_duration, value :: duration()} |
		     #parameter{id :: content_filter_property, value :: binary()} |
		     #parameter{id :: participant_guid, value :: guid()} |
		     #parameter{id :: participant_entityid, value :: entity_id()} |
		     #parameter{id :: group_guid, value :: guid()} |
		     #parameter{id :: group_entityid, value :: entity_id()} |
		     #parameter{id :: builtin_endpoint_set, value :: non_neg_integer()} |
		     #parameter{id :: property_list, value :: binary()} |
		     #parameter{id :: type_max_size_serialized, value :: integer()} |
		     #parameter{id :: entity_name, value :: binary()} |
		     #parameter{id :: key_hash, value :: binary()} |
		     #parameter{id :: status_info, value :: binary()} |
		     #parameter{id :: content_filter_info, value :: binary()} |
		     #parameter{id :: coherent_set, value :: binary()} |
		     #parameter{id :: directed_write, value :: binary()} |
		     #parameter{id :: original_writer_info, value :: binary()} |
		     #parameter{id :: 2..16#7fff, value :: binary()}.

%% OriginalWriterInfo
-type original_writer_info() :: #original_writer_info{}.

%% BuiltinEndpointSet
-type builtin_endpoint_set() :: 'DiscBuiltinEndpointParticipantAnnouncer'
			      | 'DiscBuiltinEndpointParticipantDetector'
			      | 'DiscBuiltinEndpointPublicationAnnouncer'
			      | 'DiscBuiltinEndpointPublicationDetector'
			      | 'DiscBuiltinEndpointSubscriptionAnnouncer'
			      | 'DiscBuiltinEndpointSubscriptionDetector'
			      | 'DiscBuiltinEndpointParticipantProxyAnnouncer'
			      | 'DiscBuiltinEndpointParticipantProxyDetector'
			      | 'DiscBuiltinEndpointParticipantStateAnnouncer'
			      | 'DiscBuiltinEndpointParticipantStateDetector'
			      | 'BuiltinEndpointParticipantMessageDataWriter'
			      | 'BuiltinEndpointParticipantMessageDataReader'
			      | 0 | 16#1000..16#ffffffff.

%% -type participant_message_data() :: term().

%% 
-type submessage() :: #acknack{} | #data_msg{} | #data_frag{} | #gap{} | #heartbeat{} 
		    | #heartbeat_frag{} | #info_destination{} | #info_reply{} 
		    | #info_reply_ip4{} | #info_source{} | #info_timestamp{} 
		    | #nack_frag{} | #pad{}.

-type msg() :: #msg{}.

-type available_builtin_endpoints() :: publications_reader | publications_writer |
				       subscriptions_reader | subscriptions_writer |
				       topic_reader | topic_writer | term().

%% History kind and depth
-type history_kind() :: keep_last | keep_all.
-type history_depth() :: pos_integer() | length_unlimited.
-type history_durability() :: volatile | transient_local | transient | persistent.

%% Options:
-type domain_opt() :: {port_base_number, non_neg_integer()} |
		      {domain_id_gain, pos_integer()} |
		      {participant_id_gain, pos_integer()} |
		      {d0, non_neg_integer()} |
		      {d1, non_neg_integer()} |
		      {d2, non_neg_integer()} |
		      {d3, non_neg_integer()} |
		      multicast_loop | {multicast_loop, boolean()} |
		      {mtu, pos_integer()}.

%%%===================================================================
%%% Defines and module records
%%%===================================================================

-define(SERVER, ?MODULE).

-record(data, {sup :: pid(), domains :: undefined | pid(), reg :: undefined | pid(),
	       opts :: [proplists:property()]}).

%%%===================================================================
%%% API
%%%===================================================================

start_link(Sup) ->
    gen_statem:start_link({local, ?SERVER}, ?MODULE, [Sup], []).

-spec add_domain(Domain_id) -> Result when
      Domain_id :: domain_id(),
      Result :: supervisor:startchild_ret() | {error, out_of_resources}.
%% @see add_domain/2
add_domain(Domain_id) ->
    add_domain(Domain_id, []).

-spec add_domain(Domain_id, Opts) -> Result when
      Domain_id :: domain_id(),
      Opts :: [domain_opt()],
      Result :: supervisor:startchild_ret() | {error, badarg}.
%% @doc Add a domain with domain Id.  Domains are longlived processes
%% that can be restarted for many reasons, such as errors or software
%% updates, so do not use the domain's Pid as a reference.
add_domain(Domain_id, Opts)
  when 0 =< Domain_id, Domain_id < ?MAX_DOMAINS,
       is_list(Opts) ->
    gen_statem:call(?SERVER, {add_domain, Domain_id, Opts});
add_domain(_Domain_id, _Opts) ->
    {error, badarg}.

-spec del_domain(Domain_id) -> ok when
      Domain_id :: domain_id().
%% @doc Delete a domain given its id.
del_domain(Domain_id) ->
    gen_statem:cast(?SERVER, {del_domain, Domain_id}).

-spec add_participant(Domain_id, Guid_prefix) -> Result when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Result :: supervisor:startchild_ret() | {error, Reason},
      Reason :: term(). 
%% @see add_participant/3
add_participant(Domain_id, Guid_prefix) ->
    add_participant(Domain_id, Guid_prefix, []).

-spec add_participant(Domain_id, Guid_prefix, Opts) -> Result when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Opts :: [proplists:property()],
      Result :: supervisor:startchild_ret() | {error, Reason},
      Reason :: term(). 
%% @doc Add a participant to a domain. 
add_participant(Domain_id, Guid_prefix, Opts)
  when is_bitstring(Guid_prefix), bit_size(Guid_prefix) =:= 96,
       is_list(Opts) ->
    Pid = ?MODULE:whereis(Domain_id),
    rtps_domain:add_participant(Pid, Guid_prefix, Opts).

-spec del_participant(Domain_id, Guid_prefix) -> ok when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix().
%% @doc Delete a participant from a domain.      
del_participant(Domain_id, Guid_prefix) ->
    Pid = ?MODULE:whereis(Domain_id),
    rtps_domain:del_participant(Pid, Guid_prefix).

-spec add_writer(Domain_id, Guid_prefix, Entity_key, Opts) -> Result when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_key :: entity_key(),
      Opts :: [proplists:property()],
      Result :: supervisor:startchild_ret() | {error, Reason},
      Reason :: term(). 
%% @doc Add a writer to a participant with the Topic_kind defaulting to no_key.
add_writer(Domain_id, Guid_prefix, Entity_key, Opts) ->
    add_writer(Domain_id, Guid_prefix, Entity_key, no_key, Opts).

-spec add_writer(Domain_id, Guid_prefix, Entity_key, Topic_kind, Opts) -> Result when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_key :: entity_key(),
      Topic_kind :: topic_kind(),
      Opts :: [proplists:property()],
      Result :: supervisor:startchild_ret() | {error, Reason},
      Reason :: term(). 
%% @doc Add a writer to a participant.
add_writer(Domain_id, Guid_prefix, Entity_key, Topic_kind, Opts) ->
    add_endpoint(writer, Domain_id, Guid_prefix, user, Entity_key, Topic_kind, Opts).

-spec del_writer(Domain_id, Guid_prefix, Entity_key) -> ok when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_key :: entity_key().
%% @doc Delete an endpoint from a participant.
del_writer(Domain_id, Guid_prefix, Entity_key) ->
    del_writer(Domain_id, Guid_prefix, Entity_key, no_key).

-spec del_writer(Domain_id, Guid_prefix, Entity_key, Topic_kind) -> ok when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_key :: entity_key(),
      Topic_kind :: topic_kind().
%% @doc Delete an endpoint from a participant.
del_writer(Domain_id, Guid_prefix, Entity_key, Topic_kind) ->
    del_endpoint(writer, Domain_id, Guid_prefix, user, Entity_key, Topic_kind).

-spec add_reader(Domain_id, Guid_prefix, Entity_key, Opts) -> Result when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_key :: entity_key(),
      Opts :: [proplists:property()],
      Result :: supervisor:startchild_ret() | {error, Reason},
      Reason :: term(). 
%% @doc Add a reader to a participant with Topic_kind defaulting to no_key.
add_reader(Domain_id, Guid_prefix, Entity_key, Opts) ->
    add_reader(Domain_id, Guid_prefix, Entity_key, no_key, Opts).

-spec add_reader(Domain_id, Guid_prefix, Entity_key, Topic_kind, Opts) -> Result when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_key :: entity_key(),
      Topic_kind :: topic_kind(),
      Opts :: [proplists:property()],
      Result :: supervisor:startchild_ret() | {error, Reason},
      Reason :: term(). 
%% @doc Add a reader to a participant.
add_reader(Domain_id, Guid_prefix, Entity_key, Topic_kind, Opts) ->
    add_endpoint(reader, Domain_id, Guid_prefix, user, Entity_key, Topic_kind, Opts).

-spec del_reader(Domain_id, Guid_prefix, Entity_key) -> ok when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_key :: entity_key().
%% @doc Delete an endpoint from a participant.
del_reader(Domain_id, Guid_prefix, Entity_key) ->
    del_reader(Domain_id, Guid_prefix, Entity_key, no_key).

-spec del_reader(Domain_id, Guid_prefix, Entity_key, Topic_kind) -> ok when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_key :: entity_key(),
      Topic_kind :: topic_kind().
%% @doc Delete an endpoint from a participant.
del_reader(Domain_id, Guid_prefix, Entity_key, Topic_kind) ->
    del_endpoint(reader, Domain_id, Guid_prefix, user, Entity_key, Topic_kind).

-spec add_endpoint(Kind, Domain_id, Guid_prefix, Entity_type, Entity_key, Topic_kind, Opts) -> Result when
      Kind :: writer | reader,
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_type :: entity_type(),
      Entity_key :: entity_key(),
      Topic_kind :: topic_kind(),
      Opts :: [proplists:property()],
      Result :: supervisor:startchild_ret() | {error, Reason},
      Reason :: term(). 
%% @doc Delete an endpoint from a participant.
add_endpoint(Kind, Domain_id, Guid_prefix, Entity_type, Entity_key, Topic_kind, Opts)
  when is_bitstring(Entity_key), bit_size(Entity_key) =:= 24,
       (Topic_kind =:= no_key orelse Topic_kind =:= with_key),
       is_list(Opts) ->
    Entity_kind = entity_kind(Kind, Topic_kind),
    Entity_id = #entity_id{type = Entity_type, kind = Entity_kind, key = Entity_key},
    Opts1 = [{topic_kind, Topic_kind} | Opts],
    Pid = whereis(Domain_id, Guid_prefix),
    rtps_participant:add_endpoint(Pid, Kind, Entity_id, Opts1).

%% @private
-spec del_endpoint(Kind, Domain_id, Guid_prefix, Entity_type, Entity_key, Topic_kind) -> ok when
      Kind :: writer | reader,
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_type :: entity_type(), 
      Entity_key :: entity_key(),
      Topic_kind :: topic_kind().
del_endpoint(Kind, Domain_id, Guid_prefix, Entity_type, Entity_key, Topic_kind) ->
    Entity_kind = entity_kind(Kind, Topic_kind),
    Entity_id = #entity_id{type = Entity_type, kind = Entity_kind, key = Entity_key},
    Pid = whereis(Domain_id, Guid_prefix),
    rtps_participant:del_endpoint(Pid, Entity_id).

-spec whereis(Domain_id) -> pid() | undefined when
      Domain_id :: domain_id().      
%% @doc This function returns the pid() of the domain identified by
%% Domain_id, or undefined if no such domain exists.
whereis(Domain_id) ->
    gen_statem:call(?SERVER, {whereis, Domain_id}).
    
-spec whereis(Domain_id, Guid_prefix) -> pid() | undefined when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix().
%% @doc This function returns the pid() of the participant identified
%% by Domain_id and Guid_prefix, or undefined if no such participant
%% exists.
whereis(Domain_id, Guid_prefix) ->
    Pid = ?MODULE:whereis(Domain_id),
    rtps_domain:whereis(Pid, Guid_prefix).

-spec whereis_writer(Domain_id, Guid_prefix, Entity_key) -> pid() | undefined when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_key :: entity_key().
%% @doc This function returns the pid() of a writer, identified by
%% Domain_id and Guid_prefix and Entity_key, or undefined if no such
%% endpoint exists.
whereis_writer(Domain_id, Guid_prefix, Entity_key) ->
    whereis_writer(Domain_id, Guid_prefix, Entity_key, no_key).

-spec whereis_writer(Domain_id, Guid_prefix, Entity_key, Topic_kind) -> pid() | undefined when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_key :: entity_key(),
      Topic_kind :: topic_kind().
%% @doc This function returns the pid() of a writer, identified by
%% Domain_id and Guid_prefix, Entity_key and Topic_kind, or undefined
%% if no such endpoint exists.
whereis_writer(Domain_id, Guid_prefix, Entity_key, Topic_kind) ->
    whereis(writer, Domain_id, Guid_prefix, user, Entity_key, Topic_kind).

-spec whereis_reader(Domain_id, Guid_prefix, Entity_key) -> pid() | undefined when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_key :: entity_key().
%% @doc This function returns the pid() of a reader, identified by
%% Domain_id and Guid_prefix and Entity_key, or undefined if no such
%% endpoint exists.
whereis_reader(Domain_id, Guid_prefix, Entity_key) ->
    whereis_reader(Domain_id, Guid_prefix, Entity_key, no_key).

-spec whereis_reader(Domain_id, Guid_prefix, Entity_key, Topic_kind) -> pid() | undefined when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_key :: entity_key(),
      Topic_kind :: topic_kind().
%% @doc This function returns the pid() of a reader, identified by
%% Domain_id and Guid_prefix, Entity_key and Topic_kind, or undefined
%% if no such endpoint exists.
whereis_reader(Domain_id, Guid_prefix, Entity_key, Topic_kind) ->
    whereis(reader, Domain_id, Guid_prefix, user, Entity_key, Topic_kind).

%% @private
-spec whereis(Kind, Domain_id, Guid_prefix, Entity_type, Entity_key, Topic_kind) -> pid() | undefined when
      Kind :: writer | reader,
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_type :: entity_type(), 
      Entity_key :: entity_key(),
      Topic_kind :: topic_kind().
%% @doc This function returns the pid() of the endpoint, a writer or
%% reader, identified by Domain_id, Guid_prefix, Entity_key and
%% Topic_kind, or undefined if no such endpoint exists.
whereis(Kind, Domain_id, Guid_prefix, Entity_type, Entity_key, Topic_kind) ->
    Entity_kind = entity_kind(Kind, Topic_kind),
    Entity_id = #entity_id{type = Entity_type, kind = Entity_kind, key = Entity_key},
    %% gen_statem:call(?SERVER, {whereis, Domain_id, Guid_prefix, Entity_id}).
    whereis(Domain_id, Guid_prefix, Entity_id).

-spec whereis(Domain_id, Guid_prefix, Entity_id) -> pid() | undefined when
      Domain_id :: domain_id(),
      Guid_prefix :: guid_prefix(),
      Entity_id :: entity_id().
%% @doc This function returns the pid() of the endpoint,
whereis(Domain_id, Guid_prefix, Entity_id) ->
    Pid1 = ?MODULE:whereis(Domain_id),
    Pid2 = rtps_domain:whereis(Pid1, Guid_prefix),
    rtps_participant:whereis(Pid2, Entity_id).

info() ->
    gen_statem:call(?SERVER, info).

info(Domain_id) ->
    case ?MODULE:whereis(Domain_id) of
	Pid when is_pid(Pid) ->
	    rtps_domain:info(Pid);
	_ ->
	    {error, not_found}
    end.

info(Domain_id, Guid_prefix) ->
    case whereis(Domain_id, Guid_prefix) of
	Pid when is_pid(Pid) ->
	    rtps_participant:info(Pid);
	_ ->
	    {error, not_found}
    end.

info(Domain_id, Guid_prefix, Entity_id) ->
    case whereis(Domain_id, Guid_prefix, Entity_id) of
	Pid when is_pid(Pid) ->
	    case entity_kind(Entity_id) of
		writer -> rtps_writer:info(Pid);
		reader -> rtps_reader:info(Pid);
		_ -> {error, not_found}
	    end;
	_ ->
	    {error, not_found}
    end.

-spec guid_prefix() -> Guid_prefix when
      Guid_prefix :: guid_prefix().
%% @doc Generate a new GUID. See 9.3.1.5 Mapping of the GUID_t
-dialyzer([{no_match, guid_prefix/0}]).
guid_prefix() ->
    Vendor_part = case ?VENDOR_ID of
		      unknown ->
			  <<0, 0>>;
		      _ ->
			  ?VENDOR_ID
		  end,
    Unique_part = crypto:strong_rand_bytes(10),
    <<Vendor_part/binary, Unique_part/binary>>.


-spec convtime(Time :: time()) -> integer().
%% @doc Convert from internal time representation to seconds or
%% vice versa.
convtime(Time) ->
    convtime(Time, 1).

-spec convtime_ms(Time :: time()) -> integer().
%% @doc Convert from internal time representation to milliseconds or
%% vice versa. Erlang uses milliseconds for timers etc, so this is a
%% convenient units per second.
convtime_ms(Time) ->
    convtime(Time, 1000).

-spec convtime(Time :: time(), Units_per_s :: integer()) -> integer().
%% @doc Convert from internal time representation to integer value and
%% vice versa. See Table 9.4 - PSM mapping of the value types that
%% appear on the wire (p154)
convtime(#time{seconds = Seconds, fraction = Fraction}, Units_per_s) ->
    %% time = seconds + (fraction / 2^(32))
    %% Seconds * Units_per_s + ((Fraction * Units_per_s) bsr 32);
    Seconds * Units_per_s + round(Fraction / (1 bsl 32) * Units_per_s);
convtime(zero, _) ->
    0;
convtime(infinity, _) ->
    infinity;
convtime(invalid, _) ->
    invalid;
convtime(0, _) ->
    zero;
convtime(Time, Units_per_s) when is_integer(Time) ->
    Seconds = Time div Units_per_s,
    Remainder = Time rem Units_per_s,
    Fraction = round((Remainder bsl 32) / Units_per_s),
    #time{seconds = Seconds, fraction = Fraction}.

%%%===================================================================
%%% gen_statem callbacks
%%%===================================================================

%% @private
init([Sup]) ->
    {ok, running, #data{sup = Sup, opts = []}}.

%% @private
callback_mode() ->
    [state_functions, state_enter].

%% @private
running(enter, _Old_state, #data{sup = Sup} = Data) ->
    Children = supervisor:which_children(Sup),
    {ok, Reg} = child(Children, registry),
    {ok, Domains} = child(Children, domains),
    {keep_state, Data#data{reg = Reg, domains = Domains}};
running({call, From}, {add_domain, Domain_id, Extra_opts},
	#data{reg = Reg, domains = Domains, opts = Opts} = _Data) ->
    %% The supervisor is of type one-for-all, so we can assume the pids constants.
    Child = #{id => Domain_id,
	      start => {rtps_domain_sup, start_link, [Domain_id, Reg, Extra_opts ++ Opts]},
	      restart => permanent,
	      type => supervisor},
    Reply = rtps_ofo_sup:start_child(Domains, Child),
    {keep_state_and_data, [{reply, From, Reply}]};
%% Delete a domain: this will delete all related participants and
%% endpoints belonging to the domain as well.
running(cast, {del_domain, Domain_id}, #data{domains = Domains}) ->
    rtps_ofo_sup:stop_child(Domains, Domain_id),
    keep_state_and_data;
%% Lookup domain
running({call, From}, {whereis, Domain_id}, #data{reg = Reg}) ->
    Reply = rtps_reg:whereis_name(Reg, Domain_id),
    {keep_state_and_data, [{reply, From, Reply}]};
running(Event_type, Event_content, Data) ->
    handle_event(Event_type, Event_content, running, Data).

%% @private
handle_event({call, From}, info, _State,
	     #data{sup = Sup, opts = Opts} = _Data) ->
    {ok, Reg} = rtps_sup:registry(Sup),
    Domains = [Domain_id || {Domain_id, _Pid} <-  rtps_reg:which(Reg)],
    Reply = [{domains, Domains}, {opts, Opts}],
    {keep_state_and_data, [{reply, From, Reply}]};
handle_event(enter,  _Old_state, _New_state, _Data) ->
    keep_state_and_data;
handle_event({call, From} = _Event_type, _Event_content, _State, _Data) ->
    %% ?LOG_DEBUG("Unhandled event: Type ~p, Content ~p", [Event_type,  Event_content]),
    Reply = ok,
    {keep_state_and_data, [{reply, From, Reply}]};
handle_event(_Event_type,  _Event_content, _State, _Data) ->
    %% ?LOG_DEBUG("Unhandled event: Type ~p, Content ~p", [Event_type,  Event_content]),
    keep_state_and_data.


%% @private
terminate(_Reason, _State, _Data) ->
    ok.

%% @private
code_change(_Old_vsn, State, Data, _Extra) ->
    {ok, State, Data}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

%% @private 

%% @doc Determine the value of the entity id's key field, which
%% depends on the endpoint being a writer or reader and the topic kind
%% being no_key or with_key. See "9.3.1.2 Mapping of the EntityId_t"
%% on p150 and table 9.1 on p151 for details.
entity_kind(Kind, Topic_kind) ->
    case {Kind, Topic_kind} of
	{writer, no_key} ->
	    writer_no_key;
	{writer, with_key} ->
	    writer_with_key;
	{reader, no_key} ->
	    reader_no_key;
	{reader, with_key} ->
	    reader_with_key;
	_ ->
	    %% TODO: Is this allowed / correct or should we crash?
	    unknown
    end.

%% @private
entity_kind(#entity_id{kind = writer_no_key}) ->
    writer;
entity_kind(#entity_id{kind = writer_with_key}) ->
    writer;
entity_kind(#entity_id{kind = reader_no_key}) ->
    reader;
entity_kind(#entity_id{kind = reader_with_key}) ->
    reader;
entity_kind(#entity_id{}) ->
    unknown.

%% @private
child(Children, Id) ->
    case lists:keyfind(Id, 1, Children) of
	{_Id, Child, _Type, _Modules} when is_pid(Child) ->
	    {ok, Child};
	{_Id, Child, _Type, _Modules} when Child =:= restarting ->
	    {error, Child};
	_ ->
	    {error, notfound}
    end.

